package com.tianji.learning.controller.service.impl;

import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.constants.RedisConstants;
import com.tianji.learning.controller.service.ISignRecordService;
import com.tianji.learning.domain.vo.SignResultVO;
import com.tianji.learning.mq.message.SignInMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class SignRecordServiceImpl implements ISignRecordService {
    private final StringRedisTemplate redisTemplate;
    private final RabbitMqHelper rabbitMqHelper;

    /**
     * 签到接口
     * @return
     */
    @Override
    public SignResultVO addSignRecord() {
        //1、拼接redis操作的key：前缀+userId+年月
        //获取用户id
        Long userId = UserContext.getUser();
        //获取年月
        LocalDateTime now = LocalDateTime.now();
        String format = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        //拼接key
        //key中包含userId和年月信息，表示某人在某个月的签到表
        String key = RedisConstants.SIGN_RECORD_KEY_PREFIX + userId + format;
        //2、操作redis的BitMap，使用setBit完成签到
        //获取偏移量
        //要-1是因为now.getDayOfMonth()返回的是今天是本月的第几天，返回值从1开始
        //但是bitmap的偏移量从0开始，要使bitmap中的bit位和月中的天相关联，需要减去一个1
        int offset = now.getDayOfMonth() - 1;
        //操作BitMap完成签到
        //opsForValue():这是redisTemplate提供的一个方法，表示对redis中的字符串(简单的值)的操作
        //BitMap是基于redis中的字符串类型的数据结构实现的，所以setBit是opsForValue()提供的方法
        //setBit方法是为指定键(key)上的位指定值，并返回指定位上之前的值
        //setBit中的三个参数：
        //key：表示是对哪个键进行操作，即redis中要操作的字符串
        //offset：表示设置位的偏移量，从0开始，不是从1开始
        //value：表示要设置的位值，true表示1，false表示0，因为bit位只有0和1两种值可以选择
        //返回值表示的是设置之前的值，不是表示设置是否成功，和要设置的value也无关，之和之前的位值有关
        //正常情况下没签到之前数值是0即false，返回值setBit就是false，签到完成之后将该位值变成true，表示签到过了
        //如果setBit值为true，说明设置之前的值就是true，表示已经签到过了
        Boolean setBit = redisTemplate.opsForValue().setBit(key, offset, true);
        if(setBit){//setBit为之前的数值，如果为1，即true表示已经签到过了
            throw new BadRequestException("不能重复签到");
        }
        //3、计算积分：今日签到积分+连续签到积分
        //获取连续签到天数，基于bitmap的指令实现，所以要传入key
        int days = countSignDays(key, now.getDayOfMonth());
        int rewardPoints = 0;//表示连续签到的奖励积分
        //根据连续签到天数计算奖励积分
        switch (days){
            case 7:
                rewardPoints = 7;
                break;
            case 14:
                rewardPoints = 20;
                break;
            case 28:
                rewardPoints = 40;
                break;
        }

        //4、保存积分 发送消息到mq
        rabbitMqHelper.send(
                //发送到的交换机的名字
                MqConstants.Exchange.LEARNING_EXCHANGE,
                //routine key的名字
                MqConstants.Key.SIGN_IN,
                //发送的信息，发送的是一个 SignInMessage对象，对象有两个属性userId和point(今日奖励的和连续签到奖励的)
                //of是一个类的静态的构造方法，使用of方法去创造一个实体对象
                SignInMessage.of(userId,rewardPoints + 1)
        );
        //5、封装vo返回
        SignResultVO vo = new SignResultVO();
        vo.setSignDays(days);
        vo.setSignPoints(1);
        vo.setRewardPoints(rewardPoints);
        return vo;
    }

    @Override
    public Byte[] querySignRecord() {
        //1.获取用户id
        Long userId = UserContext.getUser();
        //2.拼接key
        LocalDateTime now = LocalDateTime.now();
        String format = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = RedisConstants.SIGN_RECORD_KEY_PREFIX + userId + format;
        //3.利用bitset命令查询数据
        int days = now.getDayOfMonth();//查询天数
        List<Long> bitField = redisTemplate.opsForValue().bitField(key,
                BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(days)).valueAt(0));//从0开始获取
        if(CollUtils.isEmpty(bitField)){
            return new Byte[0];
        }
        //4.返回数组
        Long num = bitField.get(0);
        Byte[] result = new Byte[days];
        int offset = days - 1;
        while(offset>=0){
            result[offset] = (byte) (num & 1);
            num = num >>> 1;
            offset--;
        }
        return result;
    }

    /***
     * 求本月中连续签到的天数
     * @param key 缓存中的key
     * @param dayOfMonth 本月第一天到今天的天数
     * @return
     */
    private int countSignDays(String key, int dayOfMonth) {
        //1.求本月第一天到今天的所有签到的数据 bitfiled 得到的结果是十进制的
        //bitfield key get u天数 0
        List<Long> bitField = redisTemplate.opsForValue().bitField(key,
                BitFieldSubCommands.create()//这是一个静态方法，用于创建一个新的BitFieldSubCommands 实例，用于指定对bitmap的操作
                        //指定位字段操作的类型为无符号整数，字段大小为 dayOfMonth，即本月第一天到几天的天数
                        .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                        .valueAt(0)//指定了要获取值的偏移量，这里是从第 0 位开始
        );
        //如果 bitField 列表为空，返回 0，表示没有签到数据
        if(CollUtils.isEmpty(bitField)){
            return 0;
        }
        Long num = bitField.get(0);//本月第一天到今天的签到数据 拿到的是十进制
        //2.num转二进制 从后往前推有多少个1  与运算 右移
        int counter = 0;//计数器，表示连续签到的天数
        while( (num & 1 ) == 1){
            counter++;
            num = num>>>1;//右移一位
        }
        return counter;
    }
}
